package archivemanagement;

import static com.google.common.base.Throwables.propagate;
import static com.google.common.io.ByteStreams.copy;

import java.io.BufferedInputStream;
import java.io.EOFException;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map.Entry;
import java.util.Set;
import java.util.UUID;
import java.util.zip.ZipEntry;
import java.util.zip.ZipException;
import java.util.zip.ZipFile;
import java.util.zip.ZipOutputStream;

import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Unmarshaller;

import libraries.Library;
import metagrammar.Operator;
import metagrammar.OperatorDescription;
import metagrammar.Type;
import parsetree.OperatorSpec;
import util.HasId;
import util.Named;
import util.SpecWithSameOperatorCollection;

import com.google.common.base.Function;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableMap.Builder;
import com.google.common.io.Files;
import com.google.common.reflect.TypeToken;
import com.google.inject.Inject;

/**
 * An implementation of the various library interfaces ({@link TypeArchive}, {@link OperatorArchive} {@link OperatorDescriptionArchive})
 * that stores the information in a zip archive. The current implementation copies on write.
 * 
 * @author Joshua Lockerman
 */
public class ZippedArchive implements TypeArchive, OperatorArchive, OperatorDescriptionArchive,
SpecArchive, LibraryArchive {
    
    private static class IDtoString implements Function<HasId, String> {
        @Override
        public String apply(HasId input)
        {
            return input.getId().toString();
        }
    }
    
    private static class NamedtoString implements Function<Named, String> {
        @Override
        public String apply(Named input)
        {
            return input.getName();
        }
    }
    
    static {
        libraryFileType = ".zip.llib";
        String b = typeFileType = ".xml.type";
        String c = operatorFileType = ".xml.operator";
        String d = operatorDescriptionFileType = ".xml.opDesc";
        String e = operatorSpecType = ".xml.opSpec";
        String f = libraryType = ".xml.glib";
        TypeToken<Type> typeType = TypeToken.of(Type.class);
        TypeToken<Operator> operatorType = TypeToken.of(Operator.class);
        TypeToken<OperatorDescription> descriptionType = TypeToken.of(OperatorDescription.class);
        TypeToken<SpecWithSameOperatorCollection> operatorSpecType = TypeToken
        .of(SpecWithSameOperatorCollection.class);
        TypeToken<Library> libraryType = TypeToken.of(Library.class);
        Builder<TypeToken<? extends Object>, String> x = ImmutableMap
        .<TypeToken<? extends Object>, String> builder().put(typeType, b)
        .put(operatorType, c).put(descriptionType, d).put(operatorSpecType, e)
        .put(libraryType, f);
        fileTypeMap = x.build();
    }
    
    public static final String libraryFileType;
    public static final String typeFileType;
    public static final String operatorFileType;
    public static final String operatorDescriptionFileType;
    public static final String operatorSpecType;
    public static final String libraryType;
    
    private static final IDtoString idToString = new IDtoString();
    private static final NamedtoString namedToString = new NamedtoString();
    
    private static final ImmutableMap<TypeToken<? extends Object>, String> fileTypeMap;
    
    private final JAXBContext xmlContext;
    private final Path path;
    
    public ZippedArchive(File file) throws IOException
    {
        this(file.toPath());
    }
    
    @Inject
    public ZippedArchive(Path path, String name) throws IOException
    {
        this(path.resolve(name + libraryFileType));
    }
    
    private ZippedArchive(Path location)
    {
        this.path = location;
        
        try {
            xmlContext = JAXBContext.newInstance(Type.class, Operator.class,
                    OperatorDescription.class, OperatorSpec.class,
                    SpecWithSameOperatorCollection.class, Library.class, HashSet.class);
        }
        catch(JAXBException e) {
            throw new RuntimeException(e);
        }
    }
    
    @Override
    public boolean containsDescription(Operator operator) throws IOException
    {
        return containsValue(operator.getId(), TypeToken.of(OperatorDescription.class));
    }
    
    @Override
    public boolean containsOperator(UUID id) throws IOException
    {
        return containsValue(id, TypeToken.of(Operator.class));
    }
    
    @Override
    public boolean containsSpecs(Operator id) throws IOException
    {
        return containsValue(id, TypeToken.of(SpecWithSameOperatorCollection.class));
    }
    
    @Override
    public boolean containsType(UUID id) throws IOException
    {
        return containsValue(id, TypeToken.of(Type.class));
    }
    
    public <Value> String getFileType(TypeToken<Value> type)
    {
        return fileTypeMap.get(type);
    }
    
    @Override
    public Collection<OperatorDescription> loadAllDescriptions() throws IOException
    {
        return handleLoadAll(TypeToken.of(OperatorDescription.class));
    }
    
    @Override
    public Collection<Operator> loadAllOperators() throws IOException
    {
        return handleLoadAll(TypeToken.of(Operator.class));
    }
    
    @Override
    public Collection<SpecWithSameOperatorCollection> loadAllSpecs() throws IOException
    {
        return handleLoadAll(TypeToken.of(SpecWithSameOperatorCollection.class));
    }
    
    @Override
    public Collection<Type> loadAllTypes() throws IOException
    {
        return handleLoadAll(TypeToken.of(Type.class));
    }
    
    @Override
    public OperatorDescription loadDescription(Operator operator) throws IOException
    {
        return handleLoad(operator.getId().toString(), TypeToken.of(OperatorDescription.class));
    }
    
    @Override
    public Operator loadOperator(UUID key) throws IOException
    {
        return handleLoad(key.toString(), TypeToken.of(Operator.class));
    }
    
    @Override
    public Collection<OperatorSpec> loadSpecs(Operator key) throws IOException
    {
        return handleLoad(key.getId().toString(),
                TypeToken.of(SpecWithSameOperatorCollection.class));
    }
    
    @Override
    public Type loadType(UUID key) throws IOException
    {
        return handleLoad(key.toString(), TypeToken.of(Type.class));
    }
    
    @Override
    public boolean writeDescription(OperatorDescription value) throws IOException
    {
        return handleWrite(value, TypeToken.of(OperatorDescription.class));
    }
    
    @Override
    public boolean writeOperator(Operator operator) throws IOException
    {
        return handleWrite(operator, TypeToken.of(Operator.class));
    }
    
    @Override
    public boolean writeSpecs(SpecWithSameOperatorCollection... specs) throws IOException
    {
        return handleWrites(TypeToken.of(SpecWithSameOperatorCollection.class), specs);
    }
    
    @Override
    public boolean writeType(Type type) throws IOException
    {
        return handleWrite(type, TypeToken.of(Type.class));
    }
    
    private <Value> void addToZip(Function<? super Value, String> namer,
            TypeToken<Value> type, ZipOutputStream output, Value... values) throws IOException,
            JAXBException
            {
        for(int i = 0; i < values.length; i++) {
            Value val = values[i];
            ZipEntry entry = new ZipEntry(namer.apply(val) + getFileType(type));
            entry.setComment(getComment(val, type));
            output.putNextEntry(entry);
            xmlContext.createMarshaller().marshal(type, output);
        }
            }
    
    private <Value> void addToZip(Value type, String name, String comment,
            ZipOutputStream output) throws IOException, JAXBException
            {
        ZipEntry entry = new ZipEntry(name);
        entry.setComment(comment);
        output.putNextEntry(entry);
        xmlContext.createMarshaller().marshal(type, output);
            }
    
    private <Key, Value> boolean containsValue(Key key, TypeToken<Value> valueType)
    throws IOException
    {
        try {
            return (new ZipFile(path.toFile()).getEntry(key.toString() + getFileType(valueType))) != null;
        }
        catch(FileNotFoundException e) {
            return false;
        }
        catch(EOFException e) {
            return false;
        }
    }
    
    private <Value> String getComment(Value value, TypeToken<Value> type)
    {
        if(TypeToken.of(Named.class).isAssignableFrom(type)) {
            return ((Named) value).getName();
        }
        return "";
    }
    
    /**
     * @return
     * @throws ZipException
     * @throws IOException
     */
    private ZipFile getZipFileIfExistisAndNotEmpty() throws ZipException, IOException
    {
        ZipFile zipFile;
        try {
            zipFile = new ZipFile(path.toFile());
        }
        catch(EOFException e) {
            return null;
        }
        catch(FileNotFoundException e) {
            return null;
        }
        return zipFile;
    }
    
    private <Value> Value handleLoad(String key, TypeToken<Value> valueType)
    throws IOException
    {
        ZipFile zipFile = getZipFileIfExistisAndNotEmpty();
        if(zipFile == null) {
            return null;
        }
        
        String fileType = getFileType(valueType);
        ZipEntry entry = zipFile.getEntry(key.toString() + fileType);
        if(entry == null) {
            return null;
        }
        InputStream inputStream = zipFile.getInputStream(entry);
        Unmarshaller unmarshaller;
        try {
            unmarshaller = xmlContext.createUnmarshaller();
            Object found = unmarshaller.unmarshal(inputStream);
            if(valueType.isAssignableFrom(found.getClass())) {
                return (Value) found;
            }
            else {
                throw new IOException();
            }
        }
        catch(JAXBException e) {
            throw propagate(e);
        }
        finally {
            if(inputStream != null) {
                inputStream.close();
            }
        }
    }
    
    private <Value> Collection<Value> handleLoadAll(TypeToken<Value> valueType)
    throws IOException
    {
        ZipFile zipFile = getZipFileIfExistisAndNotEmpty();
        if(zipFile == null) {
            return Collections.emptyList();
        }
        
        ArrayList<Value> types = new ArrayList<Value>(zipFile.size());
        Enumeration<? extends ZipEntry> entries = zipFile.entries();
        try {
            Unmarshaller unmarshaller = xmlContext.createUnmarshaller();
            String fileType = getFileType(valueType);
            while(entries.hasMoreElements()) {
                try {
                    ZipEntry entry = entries.nextElement();
                    if(entry.getName().endsWith(fileType)) {
                        InputStream inputStream = null;
                        try {
                            inputStream = zipFile.getInputStream(entry);
                            Object found = unmarshaller.unmarshal(inputStream);
                            if(valueType.isAssignableFrom(found.getClass())) {
                                types.add((Value) found);
                            }
                        }
                        finally {
                            if(inputStream != null) {
                                inputStream.close();
                            }
                        }
                    }
                }
                catch(NullPointerException e) {
                    // for some reason entries.hasMoreElements() returns true even when there
                    // are no more elements, creating a null ponter exception
                    //FIXME
                }
            }
        }
        catch(JAXBException e) {
            throw propagate(e);
        }
        return types;
    }
    
    private <Value extends HasId> boolean handleWrite(Value value, TypeToken<Value> type)
    throws ZipException, IOException
    {
        return handleWrite(value, idToString, type);
    }
    
    private <Value> boolean handleWrite(Value value, Function<? super Value, String> namer,
            TypeToken<Value> type)
    throws ZipException, IOException
    {
        ZipOutputStream output = null;
        try {
            ZipFile zipFile = getZipFileIfExistisAndNotEmpty();
            String fileName = namer.apply(value) + getFileType(type);
            String comment = getComment(value, type);
            if(zipFile == null) {
                output = new ZipOutputStream(new FileOutputStream(path.toString()));
                addToZip(value, fileName, comment, output);
                output.close();
                return true;
            }
            boolean written = false;
            output = new ZipOutputStream(new FileOutputStream(path.toString() + ".temp"));
            Enumeration<? extends ZipEntry> entries = zipFile.entries();
            while(entries.hasMoreElements()) {
                ZipEntry entry = entries.nextElement();
                if(entry.getName().equals(fileName)) {
                    addToZip(value, fileName, comment, output);
                    written = true;
                }
                else {
                    output.putNextEntry(entry);
                    InputStream is = new BufferedInputStream(zipFile.getInputStream(entry));
                    copy(is, output);
                    is.close();
                }
            }
            if( !written) {
                addToZip(value, fileName, comment, output);
                written = true;
            }
            output.close();
            Files.move(Paths.get(path.toString() + ".temp").toFile(), path.toFile());
            return written;
        }
        catch(JAXBException e) {
            throw propagate(e);
        }
        finally {
            if(output != null) {
                output.close();
            }
        }
    }
    
    private <Value extends HasId> boolean handleWrites(TypeToken<Value> type, Value... values)
    throws ZipException, IOException
    {
        return handleWrites(idToString, type, values);
    }
    
    private <Value> boolean handleWrites(Function<? super Value, String> namer,
            TypeToken<Value> type, Value... values) throws ZipException, IOException
            {
        //TODO
        ZipOutputStream output = null;
        try {
            ZipFile zipFile = getZipFileIfExistisAndNotEmpty();
            if(zipFile == null) {
                output = new ZipOutputStream(new FileOutputStream(path.toString()));
                addToZip(namer, type, output, values);
                output.close();
                return true;
            }
            boolean written = false;
            HashMap<String, Value> nameMap = new HashMap<String, Value>(values.length);
            String fileType = getFileType(type);
            for(int i = 0; i < values.length; i++) {
                nameMap.put(namer.apply(values[i]) + fileType, values[i]);
            }
            output = new ZipOutputStream(new FileOutputStream(path.toString() + ".temp"));
            Enumeration<? extends ZipEntry> entries = zipFile.entries();
            while(entries.hasMoreElements()) {
                ZipEntry entry = entries.nextElement();
                String name = entry.getName();
                Value val = nameMap.remove(name);
                if(val != null) {
                    String comment = getComment(val, type);
                    addToZip(val, name, comment, output);
                    written = true;
                }
                else {
                    output.putNextEntry(entry);
                    InputStream is = new BufferedInputStream(zipFile.getInputStream(entry));
                    copy(is, output);
                    is.close();
                }
            }
            if( !nameMap.isEmpty()) {
                Set<Entry<String, Value>> remainingValues = nameMap.entrySet();
                for(Entry<String, Value> val : remainingValues) {
                    String comment = getComment(val.getValue(), type);
                    addToZip(val.getValue(), val.getKey(), comment, output);
                }
                written = true;
            }
            output.close();
            Files.move(Paths.get(path.toString() + ".temp").toFile(), path.toFile());
            return written;
        }
        catch(JAXBException e) {
            throw propagate(e);
        }
        finally {
            if(output != null) {
                output.close();
            }
        }
            }
    
    @Override
    public Library loadLibrary(String key) throws IOException
    {
        return handleLoad(key, TypeToken.of(Library.class));
    }
    
    @Override
    public boolean writeLibrary(Library value) throws IOException
    {
        return handleWrite(value, namedToString, TypeToken.of(Library.class));
    }
    
    @Override
    public boolean containsLibrary(String string) throws IOException
    {
        return containsValue(string, TypeToken.of(Library.class));
        
    }
    
    @Override
    public Collection<Library> loadAllLibraries() throws IOException
    {
        return handleLoadAll(TypeToken.of(Library.class));
    }
}
